package daybook.model.transaction;

import daybook.Resources;
import daybook.exception.ValidationException;
import daybook.model.Commodity;
import daybook.model.Comparators;

import java.io.Closeable;
import java.io.IOException;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collector;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class TransactionValidator implements Consumer<Transaction> {
    private final List<Transaction> transactions;
    private final Consumer<Transaction> validator;

    public TransactionValidator(Consumer<Transaction> validator) {
        this.validator = Objects.requireNonNull(validator);
        this.transactions = new ArrayList<>();
    }

    public List<Transaction> getTransactions() {
        return Collections.unmodifiableList(transactions);
    }

    public static void validate(Transaction lastTransaction, Transaction transaction, Consumer<Transaction> validator) {
        validator.accept(transaction);
        if (lastTransaction != null) {
            final int cmp = Comparators.transactionComparator.compare(lastTransaction, transaction);
            if (cmp >= 0) {
                throw ValidationException.of(Resources.ErrorMessages.UNORDERED_TRANSACTIONS,
                        lastTransaction.toPrettyString(), transaction.toPrettyString());
            }
        }
    }

    public static void validate(Transaction lastTransaction, Transaction transaction, Commodity defaultCurrency) {
        validate(lastTransaction, transaction, tx -> tx.validate(defaultCurrency));
    }

    @Override
    public void accept(Transaction transaction) {
        final Transaction lastTransaction = transactions.isEmpty() ? null : transactions.get(transactions.size() - 1);
        validate(lastTransaction, transaction, validator);
        transactions.add(transaction);
    }

    public void combine(TransactionValidator transactionValidator) {
        transactions.addAll(transactionValidator.transactions);
    }

    public static Collector<Transaction, TransactionValidator, TransactionValidator> collector(Consumer<Transaction> validator) {
        return Collector.of(
                () -> new TransactionValidator(validator),
                TransactionValidator::accept,
                (tv1, tv2) -> {
                    tv1.combine(tv2);
                    return tv1;
                }
        );
    }

    public static Collector<Transaction, TransactionValidator, TransactionValidator> collector(Commodity defaultCurrency) {
        Objects.requireNonNull(defaultCurrency);
        return collector(tx -> tx.validate(defaultCurrency));
    }

    public static List<Transaction> collect(Stream<Transaction> stream, Consumer<Transaction> validator) {
        final TransactionValidator transactionValidator = stream.collect(collector(validator));
        stream.close();
        return transactionValidator.getTransactions();
    }

    public static List<Transaction> collect(Stream<Transaction> stream, Commodity defaultCurrency) {
        final TransactionValidator transactionValidator = stream.collect(collector(defaultCurrency));
        stream.close();
        return transactionValidator.getTransactions();
    }

    public static Stream<Transaction> validate(Iterator<Transaction> iterator, Closeable closeable, Consumer<Transaction> validator) {
        Objects.requireNonNull(validator);
        final Iterator<Transaction> validatedIterator = new Iterator<Transaction>() {
            private Transaction lastTransaction = null;

            @Override
            public boolean hasNext() {
                return iterator.hasNext();
            }

            @Override
            public Transaction next() {
                final Transaction transaction = iterator.next();
                if (lastTransaction != null) {
                    final int cmp = Comparators.transactionComparator.compare(lastTransaction, transaction);
                    if (cmp >= 0) {
                        throw ValidationException.of(Resources.ErrorMessages.UNORDERED_TRANSACTIONS,
                                lastTransaction.toPrettyString(), transaction.toPrettyString());
                    }
                }
                validator.accept(transaction);
                lastTransaction = transaction;
                return transaction;
            }
        };

        final Stream<Transaction> result = StreamSupport.stream(
                Spliterators.spliteratorUnknownSize(validatedIterator,
                        Spliterator.ORDERED | Spliterator.IMMUTABLE), false);

        if (closeable != null) {
            result.onClose(() -> {
                try {
                    closeable.close();
                } catch (IOException ignore) {
                }
            });
        }

        return result;
    }

    public static Stream<Transaction> validate(Iterator<Transaction> iterator, Closeable closeable, Commodity defaultCurrency) {
        Objects.requireNonNull(defaultCurrency);
        return validate(iterator, closeable, tx -> tx.validate(defaultCurrency));
    }
}
